namespace C4F.DevKit.Bluetooth
{
    namespace ServiceAndDeviceDiscovery
    {
        #region Using Directives
        using System;
        using System.Collections.Generic;
        using System.Text;
        using System.Runtime.InteropServices;
        using System.Net.Sockets;
        using System.Threading;
        using System.Collections.Specialized;
        using System.ComponentModel;
        using Resources;
        #endregion

        /// <summary>
        /// Represents a service on remote device.        
        /// </summary>
        public sealed class RemoteService:Service
        {
            #region Private Fields
            /// <summary>
            /// Queryset structure holds the information related 
            /// to the service.
            /// </summary>
            QuerySet qset;
            /// <summary>
            /// Address of device on which this service exists.
            /// </summary>
            BluetoothSocketAddress deviceAddr;
            /// <summary>
            /// Socket used for connecting and disconnecting from the 
            /// remote service.
            /// </summary>
            Socket clientSocket;
            /// <summary>
            /// HybridDictionary used for maintaining the lifetime of an 
            /// asynchronous operation.            
            /// </summary>
            HybridDictionary userStateToLifetime = new HybridDictionary();
            #endregion
            
            #region Private Delegates And Events
            /// <summary>
            /// Represents a method to be called when a message is to be 
            /// dispatched to a synchronization context.
            /// </summary>
            SendOrPostCallback onConnectionCompletedDelegate;
            /// <summary>
            /// Represents a method to be called when a message is to be 
            /// dispatched to a synchronization context.
            /// </summary>
            SendOrPostCallback onDisconnectionCompletedDelegate;
            /// <summary>
            /// Wraps the method which performs the actual task
            /// </summary>
            /// <param name="asyncOp">Represents an asynchronous operation
            /// </param>
            delegate void WorkerEventHandler(AsyncOperation asyncOp);
            #endregion

            #region Public Delegates And Events
            /// <summary>
            /// Event handler for the Connected event.
            /// </summary>
            /// <param name="sender">The sender of the event.</param>
            /// <param name="connectionEventArgs">EventArgs when an Connection
            /// to remote service completes.</param>            
            public delegate void ConnectionEventHandler(object sender,
                                      ConnectionEventArgs connectionEventArgs);

            /// <summary>
            /// This event is fired when an Connection to a remote service 
            /// completes.
            /// </summary>
            public event ConnectionEventHandler Connected;

            /// <summary>
            /// Event handler for the Disconnected event.
            /// </summary>
            /// <param name="sender">The sender of the event.</param>
            /// <param name="disconnectionEventArgs">EventArgs when an 
            /// Disconnection from a  remote service completes.</param>
            public delegate void DisconnectionEventHandler(object sender,
                          DisconnectionEventArgs disconnectionEventArgs);

            /// <summary>
            /// This event is fired when an Disconnection from a remote service 
            /// completes.
            /// </summary>
            public event DisconnectionEventHandler Disconnected;
            #endregion

            #region Public Property
            /// <summary>
            /// Gets the device address.
            /// </summary>
            public BluetoothSocketAddress DeviceAddress
            {
                get
                {
                    return this.deviceAddr;
                }
            }
            #endregion

            #region Constructor
            /// <summary>
            /// Initializes a remote service object
            /// </summary>
            /// <param name="querySet">Structure containing service specific 
            /// information.</param>
            /// <param name="deviceAddress">Represents the device address.</param>
            /// <exception cref="ArgumentNullException">Thrown when 
            /// when a null reference (not set or empty) is passed 
            /// to a method that does not accept it as a valid argument.
            /// </exception>            
            internal RemoteService(QuerySet querySet,
                BluetoothSocketAddress deviceAddress)
                : base(querySet)
            {
                //Verify if all input parameters have been provided and are 
                //valid.
                if (querySet == null)
                    throw new ArgumentNullException
                          (SDPMessages.ErrorMsgNullQuerySet);

                if (deviceAddress == null)
                    throw new ArgumentNullException
                            (SDPMessages.ErrorMsgNullDeviceAddress);

                this.qset = querySet;
                this.deviceAddr = deviceAddress;

                //Initialize SendOrPostCallback delegates
                onConnectionCompletedDelegate = new SendOrPostCallback(ConnectionCompleted);
                onDisconnectionCompletedDelegate = new SendOrPostCallback(DisconnectionCompleted);
            }
            #endregion

            #region Private Functions

            #region ConnectAsync Helper Functions
            /// <summary>
            /// This method is invoked via the AsyncOperation, so that it
            /// executes on the correct thread.This method is called when
            /// the connection is complete and raises the Connected event
            /// </summary>
            /// <param name="operationState">Represents the ConnectionEventArgs 
            /// object.</param>
            private void ConnectionCompleted(object operationState)
            {
                ConnectionEventArgs connectionEventArgs 
                        = operationState as ConnectionEventArgs;

                //Raise the connected event.
                if (Connected != null)
                {
                    Connected(this, connectionEventArgs);
                }
               
            }          

            /// <summary>
            ///Removes the task from the lifetime collection and packages the
            /// result of the asynchronous operation in a ConnectionEventArgs 
            /// object and then ends the asynchronous call
            /// </summary>
            /// <param name="networkStream">Used for communicating with the service.
            /// </param>
            /// <param name="exception">An error that occurred during the 
            /// asynchronous operation.</param>
            /// <param name="cancelled">A value indicating whether the 
            /// asynchronous operation was cancelled.</param>
            /// <param name="asyncOp">Represents the asynchronous operation.
            /// </param>
            
            private void ConnectCompletionMethod(NetworkStream networkStream,
                                                Exception exception, 
                                                bool cancelled, 
                                                AsyncOperation asyncOp)
            {

                // If the task was not previously canceled,
                // remove the task from the lifetime collection.
                if (!cancelled)
                {
                    lock (userStateToLifetime.SyncRoot)
                    {
                        userStateToLifetime.Remove(asyncOp.UserSuppliedState);
                    }
                }

                // Package the results of the operation in a 
                // ConnectionEventArgs.
                ConnectionEventArgs connectionEventArgs =
                    new ConnectionEventArgs(networkStream, 
                                            exception, 
                                            cancelled, 
                                            asyncOp.UserSuppliedState);

                // End the task. The asyncOp object is responsible 
                // for marshaling the call.
                // Note that after the call to OperationCompleted, 
                // asyncOp is no longer usable, and any attempt to use it
                // will cause an exception to be thrown.        
                asyncOp.PostOperationCompleted(onConnectionCompletedDelegate, 
                                                         connectionEventArgs);

                           
                
            }
            /// <summary>
            /// This method actucally performs the task of connecting to the 
            /// service i.e. creating an endopint for the service and then 
            /// connecting to it.
            /// </summary>
            /// <param name="asyncOp">Represents the asynchronous operation.
            /// </param>            
            private void ConnectToService(AsyncOperation asyncOp)
            {
                Exception exception = null;
                NetworkStream networkStream = null;
                if (!TaskCanceled(asyncOp.UserSuppliedState))
                {
                    try
                    {
                        //Create a bluetooth socket
                        clientSocket 
                            = new Socket((AddressFamily)32, 
                                          SocketType.Stream, 
                                          ProtocolType.Ggp);
                        
                        this.deviceAddr.ServiceClassId 
                                            = this.Type.ServiceGuid;

                        BluetoothEndPoint endPoint 
                            = new BluetoothEndPoint(this.deviceAddr, 
                                             this.deviceAddr.ServiceClassId);
                                             clientSocket.Connect(endPoint);                        

                        networkStream = new NetworkStream(clientSocket, true);
                    }
                    catch (Exception ex)
                    {
                        ServiceConnectionException serviceEx = 
                         new ServiceConnectionException
                                (SDPMessages.ErrorMsgServiceUnableToConnect 
                                 + ex.Message);
                        exception = serviceEx;
                    }
                }
                this.ConnectCompletionMethod(networkStream, 
                                      exception,
                                      TaskCanceled(asyncOp.UserSuppliedState),
                                      asyncOp);
            }          
            #endregion

            #region DisconnectAsync Helper Functions

            /// <summary>
            /// This method is invoked via the AsyncOperation, so that it 
            /// executes on the correct thread.This method is called when 
            /// the disconnection is complete and raises the Disconnected 
            /// event.
            /// </summary>
            /// <param name="operationState">Represents the DisconnectionEventArgs 
            /// object.</param>
            private void DisconnectionCompleted(object operationState)
            {
                DisconnectionEventArgs disconnectEventArgs = 
                    operationState as DisconnectionEventArgs;
                if (Disconnected != null)
                {
                    Disconnected(this, disconnectEventArgs);
                }
            }           
            /// <summary>
            /// Removes the task from the lifetime collection and packages the
            /// result of the asynchronous operation in a 
            /// DisconnectionEventArgs object and then ends the asynchronous 
            /// call
            /// </summary>
            /// <param name="exception">An error that occurred during the 
            /// asynchronous operation.</param>
            /// <param name="canceled">A value indicating whether the 
            /// asynchronous operation was cancelled.</param>
            /// <param name="asyncOp">Represents the asynchronous operation.
            /// </param>
            private void CompletionMethodForDisconnect(Exception exception, 
                                                        bool canceled, 
                                                        AsyncOperation asyncOp)
            {

                // If the task was not previously canceled,
                // remove the task from the lifetime collection.
                if (!canceled)
                {
                    lock (userStateToLifetime.SyncRoot)
                    {
                        userStateToLifetime.Remove(
                            asyncOp.UserSuppliedState);
                    }
                }

                // Package the results of the operation in a 
                // CalculatePrimeCompletedEventArgs.
                DisconnectionEventArgs disconnectEventArgs =
                    new DisconnectionEventArgs(exception,
                                                canceled,
                                                asyncOp);

                // End the task. The asyncOp object is responsible 
                // for marshaling the call.
                // Note that after the call to OperationCompleted, 
                // asyncOp is no longer usable, and any attempt to use it
                // will cause an exception to be thrown. 

                asyncOp.PostOperationCompleted(onDisconnectionCompletedDelegate,
                                               disconnectEventArgs);

                
            }
            /// <summary>
            /// Performs the task of disconnecting from the service.
            /// </summary>
            /// <param name="asyncOp">Represents the asynchronous operation.
            /// </param>
            private void DisconnectFromService(AsyncOperation asyncOp)
            {
                Exception exception = null;

                if (!TaskCanceled(asyncOp.UserSuppliedState))
                {
                    try
                    {
                        clientSocket.Disconnect(false);
                    }
                    catch (Exception ex)
                    {
                        ServiceConnectionException serviceEx =
                         new ServiceConnectionException
                                (SDPMessages.ErrorMsgServiceUnableToDisconnect
                                 + ex.Message);
                        exception = serviceEx;
                    }
                    
                }
                this.CompletionMethodForDisconnect(exception, 
                    TaskCanceled(asyncOp.UserSuppliedState), asyncOp);
            }

            #endregion

            #region Common Helper Function
            /// <summary>
            /// This method verifies that the specified asynchrnous operation 
            /// identified by the taskId is present in the lifetime collection
            /// </summary>
            /// <param name="taskId">Identifies the asynchronous operation in
            /// the lifetime collection.</param>
            /// <returns>Returns true if task is cancelled else returns false.
            /// </returns>
           
            private bool TaskCanceled(object taskId)
            {
                return (userStateToLifetime[taskId] == null);
            }
            #endregion

            #endregion

            #region Public Functions
            /// <summary>
            /// Synchronous method for connecting to the service.
            /// </summary>
            /// <returns>Returns the network stream used for communicating with 
            /// the service.</returns>
            /// <exception cref="ServiceConnectionException">Thrown when 
            /// an error occurs while connecting the service.</exception>            
            public NetworkStream Connect()
            {
                NetworkStream networkStream = null;
                try
                {
                    //Create a bluetooth socket
                    clientSocket = new Socket((AddressFamily)32,
                                               SocketType.Stream,
                                               ProtocolType.Ggp);

                    this.deviceAddr.ServiceClassId = this.Type.ServiceGuid;

                    BluetoothEndPoint endPoint =
                        new BluetoothEndPoint(this.deviceAddr,
                                this.deviceAddr.ServiceClassId);

                    clientSocket.Connect(endPoint);

                    // the network stream will own the socket so that it will 
                    //clean up nicely

                    networkStream = new NetworkStream(clientSocket, true);

                    if (Connected != null)
                        Connected(this, new ConnectionEventArgs(networkStream, 
                                                                null, 
                                                                false, 
                                                                null));

                    return networkStream;
                }

                catch (Exception ex)
                {
                    ServiceConnectionException serviceEx = 
                        new ServiceConnectionException(
                            SDPMessages.ErrorMsgServiceUnableToConnect
                            + ex.Message);

                    if (Connected != null)
                        Connected(this, new ConnectionEventArgs(null, 
                                                                serviceEx, 
                                                                false, 
                                                                null));
                    throw serviceEx;
                }
            }
            /// <summary>
            /// Asynchronous method for connecting to the service.
            /// </summary>
            /// <param name="taskId">Identifies the asynchronous operation in
            /// the lifetime collection.</param>
            /// <exception cref="ServiceConnectionException">Thrown when 
            /// an error occurs while connecting to the service.</exception>
            /// <exception cref="ArgumentNullException">Thrown when 
            /// when a null reference (not set or empty) is passed 
            /// to a method that does not accept it as a valid argument.
            /// </exception> 
            /// <exception cref="ArgumentException">Thrown 
            /// when one of the arguments provided to a method is not valid
            /// </exception> 
            public void ConnectAsync(object taskId)
            {
                if (taskId == null)
                    throw new ArgumentNullException(
                        SDPMessages.ErrorMsgTaskId);
                try
                {
                    // Create an AsyncOperation for taskId.
                    AsyncOperation asyncOp =
                        AsyncOperationManager.CreateOperation(taskId);

                    // Multiple threads will access the task dictionary,
                    // so it must be locked to serialize access.
                    lock (userStateToLifetime.SyncRoot)
                    {
                        if (userStateToLifetime.Contains(taskId))
                        {
                            throw new ArgumentException(
                              SDPMessages.ErrorMsgTaskIdUnique,
                              SDPMessages.ErrorMsgTaskId);
                        }

                        userStateToLifetime[taskId] = taskId;
                    }

                    // Start the asynchronous operation.
                    WorkerEventHandler workerDelegate =
                        new WorkerEventHandler(ConnectToService);
                    workerDelegate.BeginInvoke(asyncOp, null, null);
                }
                catch (Exception ex)
                {
                    ServiceConnectionException serviceEx =
                      new ServiceConnectionException(
                       SDPMessages.ErrorMsgServiceUnableToConnect
                        + ex.Message);
                    throw serviceEx;
                }
            }          
            /// <summary>
            /// Synchronous method for disconnecting.
            /// </summary>         
            /// <exception cref="ServiceConnectionException">Thrown when 
            /// an error occurs while disconnecting from the service.</exception>            
            public void Disconnect()
            {
                try
                {
                    clientSocket.Disconnect(false);
                    if(Disconnected != null)
                        Disconnected(this, new DisconnectionEventArgs(null, 
                                                                      false, 
                                                                      null));
                   
                }
                catch (Exception ex)
                {
                    ServiceConnectionException serviceEx =
                     new ServiceConnectionException(
                         SDPMessages.ErrorMsgServiceUnableToDisconnect
                         + ex.Message);

                    if (Disconnected != null)
                        Disconnected(this, new DisconnectionEventArgs(serviceEx, 
                                                                      false, 
                                                                      null));

                    throw serviceEx;
                }
            }
            /// <summary>
            /// Asynchronous method for disconnecting from the service.
            /// </summary>
            /// <param name="taskId">Identifies the asynchronous operation in
            /// the lifetime collection.</param>
            /// <exception cref="ServiceConnectionException">Thrown when 
            /// an error occurs while disconnecting from the service.</exception>
            /// <exception cref="ArgumentException">Thrown 
            /// when one of the arguments provided to a method is not valid
            /// </exception> 
            public void DisconnectAsync(object taskId)
            {                
                    // Create an AsyncOperation for taskId.
                    AsyncOperation asyncOp =
                        AsyncOperationManager.CreateOperation(taskId);

                    // Multiple threads will access the task dictionary,
                    // so it must be locked to serialize access.
                    lock (userStateToLifetime.SyncRoot)
                    {
                        if (userStateToLifetime.Contains(taskId))
                        {
                            throw new ArgumentException(
                            SDPMessages.ErrorMsgTaskIdUnique,
                            SDPMessages.ErrorMsgTaskId);
                        }

                        userStateToLifetime[taskId] = asyncOp.UserSuppliedState;
                    }

                    // Start the asynchronous operation.
                    try
                    {
                        WorkerEventHandler workerDelegate =
                            new WorkerEventHandler(DisconnectFromService);
                        workerDelegate.BeginInvoke(asyncOp, null, null);
                    }
                    catch (Exception ex)
                    {
                        ServiceConnectionException serviceEx =
                         new ServiceConnectionException(
                             SDPMessages.ErrorMsgServiceUnableToDisconnect
                             + ex.Message);
                        throw serviceEx;
                    }
                
            }           
            /// <summary>
            /// Cancels the asynchronous operation.
            /// </summary>
            /// <param name="taskId">Identifies the asynchronous operation in
            /// the lifetime collection.</param>
            /// <exception cref="InvalidAsynchronousStateException">Thrown when 
            /// an error occurs while cancelling a non-existent operation.</exception>
            /// <exception cref="ArgumentNullException">Thrown when 
            ///  a null reference (not set or empty) is passed 
            /// to a method that does not accept it as a valid argument.
            /// </exception> 
            public void CancelAsync(object taskId)
            {
                if (taskId == null)
                    throw new ArgumentNullException
                                (SDPMessages.ErrorMsgTaskId);

                AsyncOperation asyncOp = 
                    userStateToLifetime[taskId] as AsyncOperation;

                if (asyncOp != null)
                {
                    lock (userStateToLifetime.SyncRoot)
                    {
                        userStateToLifetime.Remove(taskId);
                    }
                }
                else
                   throw new InvalidAsynchronousStateException
                                        (SDPMessages.ErrorMsgInvalidTaskId);
                
            }
            /// <summary>
            /// Verifies if the service is connected.
            /// </summary>
            /// <returns>Returns true if connected else returns false.</returns>
            public bool IsConnected()
            {
                if (clientSocket == null)
                    return false;
                else
                    return clientSocket.Connected;
            }
            #endregion

        }
    }
}
